from datetime import datetime

from django.shortcuts import render_to_response, get_object_or_404
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponse
from django.template import RequestContext
from django.views.decorators.http import require_POST
from django.utils import simplejson as json
from django.template.defaultfilters import slugify
from django.utils.datastructures import SortedDict
from django.views.decorators.cache import cache_control
import dateutil.parser

from google.appengine.api.images import get_serving_url
from google.appengine.api import memcache

from filetransfers.api import prepare_upload

from .models import Pillar, DeathCount
from .forms import PillarForm, DeathCountForm, UploadFileForm
from .forms import PillarForm, DeathCountForm, UploadFileForm

from dateutil import rrule

from settings import HTTP_CACHE_TIME, MC_CACHE_TIME

import logging
logger = logging.getLogger(__name__)

def get_or_none(model, **kwargs):
    try:
        return model.objects.get(**kwargs)
    except model.DoesNotExist:
        return None

def latest_or_none(model, **kwargs):
    try:
        return model.objects.latest(**kwargs)
    except model.DoesNotExist:
        return None

def dictToXml(d):
    from xml.sax.saxutils import escape

    def unicodify(o):
        if o is None:
            return u'';
        return unicode(o)

    lines = []

    def addDict(node, offset):
        for name, value in node.iteritems():
            if isinstance(value, dict):
                lines.append(offset + u"<%s>" % name)
                addDict(value, offset + u" " * 4)
                lines.append(offset + u"</%s>" % name)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, dict):
                        lines.append(offset + u"<%s>" % name)
                        addDict(item, offset + u" " * 4)
                        lines.append(offset + u"</%s>" % name)
                    else:
                        lines.append(offset + u"<%s>%s</%s>" % (name, escape(unicodify(item)), name))
            else:
                lines.append(offset + u"<%s>%s</%s>" % (name, escape(unicodify(value)), name))

    addDict(d, u"")
    lines.append(u"")
    return u"\n".join(lines)

def ReadLines(s):
  ''' Parse s into a list of comma-delimited strings.  We just have to worry about quotation marks and unicode.'''
  result = []
  decoded = unicode(s,'utf-8')
  inquote = False
  cell = u""
  line = []
  lastchar = None
  for c in decoded:
    # consume double-quotes
    if c == u'"':
      if inquote:
        inquote = False
      else:
        # two consecutive quotes emit a single quote.
        if lastchar == c:
          cell = cell + c
        inquote = True
      lastchar = c;
    # parse commas
    elif c == u',':
      lastchar = c;
      if inquote:
        cell = cell + c
      else:
        lastchar = None
        line.append(cell)
        cell = u""

    # parse newlines
    elif c == u'\n':
      if inquote:
        lastchar = c;
        cell = cell + c
      else:
        line.append(cell)
        result.append(line)
        cell = u""
        line = []
        lastchar = None;
    # eveyrthing else goes to the current cell and last char.
    else:
      lastchar = c
      cell = cell + c
  return result

def ColumnifyCSV(lines):
  result = []
  cur = {}
  # skip over header line
  for line in lines[1:]:
    cur["EnglishName"] = line[0]
    cur["ArabicName"] = line[1]
    cur["Relationship"] = line[2]
    cur["Description"] = line[3]
    cur["ArabicDescription"] = line[4]
    cur["Importance"] = line[5]
    cur["State"] = line[6]
    cur["DefectionDate"] = line[7]
    cur["DefectionURL"] = line[8]
    cur["ContextURL"] = line[9]
    cur["ArabicContextURL"] = line[10]
    cur["PhotoURL"] = line[11]
    result.append(cur)
    cur = {}
  return result

# HACK: this is hardwired to some preset category names. TODO: make this work
# from the real translation table.
def translate_category(category_name):
  return category_name

def calculate_categories():
    pillars = Pillar.objects.all()
    category_list = []
    for pillar in pillars:
        for category in pillar.categories:
            category_list.append(category.strip())
    category_list = list(set(category_list))
    return category_list

@cache_control(public=True, max_age=HTTP_CACHE_TIME)
def index(request):

    category_list = calculate_categories()

    # DEFECTIONS
    defections = Pillar.objects.filter(state = "Defected").order_by("defection_date")

    # make a list of the categories & counts
    defection_categories = SortedDict()
    for defection in defections:
        # If we don't know the date of the defection, assume it was today.
        if defection.defection_date == None:
            defection.defection_date = datetime.now()
        for defection_category_messy in defection.categories:
            defection_category = defection_category_messy.strip()
            defection_count = defection_categories.get(defection_category,0)
            defection_categories.update({
                defection_category:defection_count + 1
            })
    defection_categories.update({
        'total':Pillar.objects.filter(state = "Defected").count()
    })

    if len(defections) > 0:
        earliest_defection = defections[0]
        earliest_defection.defection_first_day_of_month = datetime(earliest_defection.defection_date.year,earliest_defection.defection_date.month,1)
    else:
        earliest_defection = None

    defection_months = SortedDict()
    defection_count = 0

    if earliest_defection:
        for defection_month in rrule.rrule(rrule.MONTHLY, dtstart=earliest_defection.defection_first_day_of_month, until=datetime.now()):
            for defection in defections:
                if defection.defection_date.year == defection_month.year and defection.defection_date.month == defection_month.month:
                    defection_count = defection_count + 1
            defection_months.update(
                {defection_month:defection_count},
            )

    # DEATH COUNT
    death_count = latest_or_none(DeathCount)
    if death_count:
        death_count = death_count.death_count

    template_vars = {
        "defection_categories":defection_categories,
        "defection_months":defection_months,
        "death_count":death_count,
        "category_list":category_list,
    }

    return render_to_response("index.html", template_vars, RequestContext(request))


@cache_control(public=True, max_age=HTTP_CACHE_TIME)
def graph(request, format):
    node_map = { }
    category_list = []
    edge_list = []
    
    nodes_edges = memcache.get("%s/graph" % request.LANGUAGE_CODE)

    if nodes_edges is None:
    
        pillars = Pillar.objects.all().order_by("-defection_date")

        pillar_id = 0

        for pillar in pillars:

            pillar_id += 1

            classes = []
            classes.append(pillar.state)
            if pillar_id <= 3:
                classes.append("DefectedRecently")

            node_id = "n-%d" % pillar.id

            for category in pillar.categories:
                category_list.append(category.strip())
                edge = {
                    "from":node_id,
                    "to":"c-%s" % slugify(category),
                    "weight":2,
                }
                edge_list.append(edge)

            if len(pillar.categories) > 1:
              pinned = True
            else:
              pinned = False

            if pinned:
              node = {
                  "name":pillar.name,
                  "className":" ".join(classes),
                  "importance":pillar.importance,
                  "x":0,
                  "y":0,
                  "pinned":True
                  }
            else:
              node = {
                  "name":pillar.name,
                  "className":" ".join(classes),
                  "importance":pillar.importance,
                  }
            node_map[node_id] = node

        category_list = list(set(category_list))
        for category in category_list:
            node = {
                "name":translate_category(category),
                "className":"category",
                "importance":8,
                }
            node_id = "c-%s" % slugify(category)
            node_map[node_id] = node

        # output the json
        nodes_edges = {
            "nodes":node_map,
            "edges":edge_list,
        }
        memcache.add("%s/graph" % request.LANGUAGE_CODE, nodes_edges, MC_CACHE_TIME)

    if format == "json":
        output = json.dumps(nodes_edges)
        content_type = "application/json"
    elif format == "xml":
        output = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n" + dictToXml(nodes_edges) + "</response>"
        content_type = "application/xml"

    return HttpResponse(output, mimetype = content_type)


# TODO: return the full details for the node; for use in the mini-profile panel.
@cache_control(public=True, max_age=HTTP_CACHE_TIME)
def node(request, format):
    pillar = get_object_or_404(Pillar, id = request.GET['id'])
    node_info = {
        'name':pillar.name,
        'categories':pillar.categories,
        'image_url':pillar.image_url,
        'state':pillar.state,
        'defection_url':pillar.defection_url,
        'context_url':pillar.context_url,
        'short_description':pillar.short_description,
        'long_description':pillar.long_description,
        # 'image_thumbnail':pillar.image_thumbnail,
        }

    if(pillar.defection_date):
      node_info['defection_date'] = pillar.defection_date.strftime('%D');

    if format == "json":
        output = json.dumps(node_info)
        content_type = "application/json"
    elif format == "xml":
        output = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n" + dictToXml(node_info) + "</response>"
        content_type = "application/xml"
    elif format == "html":
        return render_to_response("node.html", {"pillar":pillar}, RequestContext(request))

    return HttpResponse(output, mimetype = content_type)


def admin(request):

    death_count = latest_or_none(DeathCount)
    if death_count:
        death_count = death_count.death_count

    template_vars = {
        "is_admin_section":True,
        "pillars":Pillar.objects.all().order_by("name"),
        "death_count":death_count,
        "death_count_form":DeathCountForm()
    }

    return render_to_response("admin/admin_index.html", template_vars, RequestContext(request))


@require_POST
def admin_death_count(request):

    form = DeathCountForm(request.POST)
    if form.is_valid():
        new_death_count = form.save()
    else:
        return HttpResponseBadRequest()

    return HttpResponseRedirect(reverse("admin"))


def admin_pillar(request, pillar_id = None):

    pillar = get_object_or_404(Pillar, id = pillar_id)

    template_vars = {
        "is_admin_section":True,
        "pillar":pillar,
    }
    return render_to_response("admin/admin_pillar.html", template_vars, RequestContext(request))


def admin_pillar_edit(request, pillar_id = None):

    if pillar_id:
        pillar = get_object_or_404(Pillar, id = pillar_id)
        form_action, form_action_data = prepare_upload(request, reverse("admin_pillar_edit", kwargs = {'pillar_id':pillar.id}))
    else:
        pillar = None
        form_action, form_action_data = prepare_upload(request, reverse("admin_pillar_edit"))
    
    if request.method == "POST":
        form = PillarForm(request.POST, instance=pillar)
        if form.is_valid():
            new_pillar = form.save(commit=False)
            if request.FILES.get("image"):
                new_pillar.image_blobkey = str(request.FILES.get("image").blobstore_info.key())
                new_pillar.image_url = get_serving_url(blob_key=new_pillar.image_blobkey)
            # for http://code.google.com/p/googleappengine/issues/detail?id=2749
            new_pillar.long_description = new_pillar.long_description.replace("\r\n","\n")
            new_pillar.save()
            return HttpResponseRedirect(reverse("admin_pillar", kwargs = {'pillar_id':new_pillar.id}))
    else:
        form = PillarForm(instance=pillar)

    template_vars = {
        "is_admin_section":True,
        "pillar":pillar,
        "form":form,
        "form_action":form_action,
    }
    return render_to_response("admin/admin_form.html", template_vars, RequestContext(request))


def pillar_from_csv_row(d, pil, errors):
    try:
        name = d["EnglishName"]
        pil.name = d["EnglishName"]
        pil.name_ar =d["ArabicName"]
        pil.categories =d["Relationship"].split(",")
        pil.long_description=d["Description"]
        pil.long_description_ar=d["ArabicDescription"]
        pil.state=d["State"]
        # pil.image_url=d["PhotoURL"].encode('ascii')
        pil.context_url=d["ContextURL"].encode('ascii')
        pil.context_url_ar=d["ArabicContextURL"].encode('ascii')
        pil.defection_url=d["DefectionURL"].encode('ascii')
        # TODO: allow getting images from the web & blob/rescale them.
        if d["Importance"] == "":
            pil.importance=2
        else:
            pil.importance=int(d["Importance"])
        if d["DefectionDate"] != "":
            pil.defection_date = dateutil.parser.parse(d["DefectionDate"])
        elif pil.state == "Defected":
            errors.append("Warning: %s has status 'Defected' but lacks a defection date. Using todays date as date of defection." % d["EnglishName"])
            pil.defection_date = datetime.now()
        return True
    except:
      errors.append("Failed to validate pillar entry: %s" % str(d))
      logger.warning("Failed to validate pillar on file upload: %s" % str(d))
      return False

def admin_pillar_massupload(request):
    form_action, form_action_data = prepare_upload(request, reverse("admin_pillar_massupload"))
    errors=[]
    messages=[]
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        # don't bother with anything bigger than 10 megs.
        if form.is_valid() and (request.FILES['docfile'] is not None
                                and request.FILES['docfile'].size < 1e7):
            thefile = request.FILES['docfile']
            dictsForAll = ColumnifyCSV(ReadLines(thefile.read()))
            ps = Pillar.objects.all();
            name_ps = {}
            for p in ps:
                name_ps[p.name] = p

            for d in dictsForAll:
                name = d['EnglishName']
                if (len(name) > 0):
                    if name_ps.has_key(name):
                        messages.append("Replacing: %s" % name)
                        if pillar_from_csv_row(d = d, pil = name_ps[name], errors = errors):
                            name_ps[name].save()
                        del name_ps[name]
                    else:
                        pil = Pillar()
                        messages.append("New: %s" % name)
                        if pillar_from_csv_row(d = d, pil = pil, errors = errors):
                            pil.save()
                else:
                    errors.append("Your CSV file has an entry without an english name, this has been ignored.")

            for p in name_ps.values():
                messages.append("Deleting: %s" % p.name)
                p.delete();

            if len(errors) == 0:
                messages.append("File uploaded successfully!")
    else:
        form = UploadFileForm()

    template_vars = {
        "is_admin_section":True,
#        "form_action":form_action,
        "form":form,
        "messages":messages,
        "errors":errors
    }
    return render_to_response("admin/admin_form.html", template_vars, RequestContext(request))
